﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq;
using System.Text;
using CombinatorialObjects;
using Salesman;
using System.Threading;
using System.Threading.Tasks;

namespace Plan
{
    public class ScheduleSolution
    {
        private readonly ScheduleInstance _instance;
        private Permutation _perm;
        private Criteria _usedCriteria = Criteria.All;

        [Flags]
        public enum Criteria
        {
            GapsAndWalks = 1,
            All = 0xFFF
        }

        public ScheduleSolution(ScheduleInstance instance)
        {
            _instance = instance;
            _perm = new Permutation(instance.Size);
        }

        private ScheduleSolution(ScheduleInstance instance, Permutation perm)
        {
            _instance = instance;
            _perm = perm;
        }

        public ScheduleInstance Instance
        {
            get { return _instance; }
        }
        /// <summary>
        /// _instance.GroupCount * _instance.SubjectPerGroup
        /// </summary>
        int InTimeElements
        {
            get
            {
                return _instance.DayCount * _instance.LessonsPerDay * _instance.ClassroomsCount;
            }
        }
        // n -- pozycja w permutacji
        internal GroupSubject GetGroupSubject(int n)
        {
            int v = _perm[n];
            var gs = new GroupSubject
            {
                Subject = v % _instance.SubjectCount,
                Time = n % _instance.TotalTime,
                Classroom = n / _instance.TotalTime,
                IsTimeUsed = n < InTimeElements,
            };
            gs.Day = gs.Time / _instance.LessonsPerDay;
            gs.Group = v / _instance.SubjectCount / _instance.TeacherCount;
            gs.Teacher = (v / _instance.SubjectCount) % _instance.TeacherCount;
            gs.TimeOfDay = gs.Time % _instance.LessonsPerDay;
            if (v < _instance.M.Length)
                gs.IsValid = _instance.M[gs.Group, gs.Teacher, gs.Subject];
            return gs;
        }
        /// <summary>
        /// Losowy indeks permutacji, który ma sens, czyli instance.M[i] = true;
        /// </summary>
        /// <param name="generator">Generator liczb losowych</param>
        public int GetRandomIndex(Random generator)
        {
            return GetRandomIndexExcluding(generator, null);
        }
        /// <summary>
        /// Losowy indeks permutacji, który ma sens, czyli instance.M[i] = true;
        /// </summary>
        /// <param name="generator">Generator liczb losowych</param>
        /// <param name="reservedIndex">Indeks, którego nie uwzględniać</param>
        public int GetRandomIndexExcluding(Random generator, int? reservedIndex)
        {
            int n = generator.Next(_instance.MeaningFullValusCount - 1);
            int i = 0;
            for (; i < _instance.M.Length && n > 0; i++)
            {
                if (_perm[i] < _instance.MeaningFullValusCount && (reservedIndex == null || i != reservedIndex) && _instance.GetIthMElement(_perm[i]))
                    n--;
            }
            if (i == _perm.Order)
                i = 0;
            return _perm[i];
        }

        IEnumerable<GroupSubject> GetPlanInternal()
        {

            for (int i = 0; i < _perm.Order; i++)
            {
                var gs = GetGroupSubject(i);
                yield return gs;
            }
        }
        public IEnumerable<GroupSubject> GetPlanCleared()
        {
            foreach (var gs in GetPlan()) yield return gs;
            /*
            for (int i = 0; i < _perm.Order; i++)
            {
                GroupSubject gs = GetGroupSubject(i);
              //  if (usedSubjects[gs.Group][gs.Subject])
            //        gs.IsValid = false;
             //   usedSubjects[gs.Group][gs.Subject] = true;
                if (gs.IsValid && gs.IsTimeUsed)
                    yield return gs;
            }*/
        }

        public IEnumerable<GroupSubject> GetPlan()
        {
            foreach (var gs in GetPlanInternal()) if (gs.IsValid && gs.IsTimeUsed) yield return gs;
            /*
            for (int i = 0; i < _perm.Order; i++)
            {
                GroupSubject gs = GetGroupSubject(i);
                if (gs.IsValid && gs.IsTimeUsed)
                    yield return gs;
            }*/
        }
        StringBuilder _shitBuilder = new StringBuilder();
        public string Shits;

        internal int GetGapsAndWalks(IEnumerable<GroupSubject> sortedGs, bool doShit = false)
        {
            StringBuilder _localShitBuilder = new StringBuilder();
            int gap = 0;
            GroupSubject prev = sortedGs.First();
            int lessonsInDay = 0;

            const int SCHIZOFRENIA_PENALTY = 40; // kara za to, że klasa ma zajęcia w dwóch miejscach w tym samym czasie.
            //const int MOVE_ASS_PENALTY = 1; // kara za to, że grupa musi się przenieść z jednej sali do drugiej
            const int GAP_PENALTY = 3;
            const int INCORRECT_SUBJECT_PENALTY = 1; // kara za to, że w sali dedykowanej przedmiotowi przeprowadza się inne zajęcia
            const int INCORRECT_ELEMENT_PENALTY = 30; // kara za to, że dana grupa nie powinna mieć tych zejęć lub jest nieprawidłowy nauczyciel (!)
            const int DUPLICATE_SUBJECT_PENALTY = 20; // kara za to, że dana grupa dwa lub więcej razy robi ten sam przedmiot
            const int INCORRECT_ROOM_PENALTY = 8; // kara za to, że przedmiot dedykowany odpowiedniej sali jest przeprowadzony w innym miejscu
            //const int NOT_MORNING_PENALTY = 1; // kara za to, że zajęcia nie rozpoczęły się rano
            int optimalLessonCount = _instance.OptimalLessonCount;


            var withoutFirst = sortedGs.Skip(1).ToArray();
            bool[] usedSubjects = new bool[_instance.SubjectCount];
            //if (!sortedGs.First().IsValid)
            // gap += INCORRECT_ELEMENT_PENALTY;

            for (int i = 0; i < withoutFirst.Length; i++)
            {
                GroupSubject gs = withoutFirst[i];
                //if (usedSubjects[gs.Subject])
                  //  gs.IsValid = false;
                if (!gs.IsValid)
                    continue;

                // if gs.IsValid then:

                if (!gs.IsTimeUsed)
                {
                    gap += INCORRECT_ELEMENT_PENALTY;
                    if (doShit) _localShitBuilder.AppendFormat("not used: {0} (+{1})", gs, INCORRECT_ELEMENT_PENALTY).AppendLine();

                    continue;
                }

                // if gs.IsValid & gs.IsTimeUsed then:

                int localGap = (gs.Time - prev.Time);
                if (usedSubjects[gs.Subject])
                {
                    /* int j = i + 1;
                     bool quiteOk = true;
                     // jeśli do końca dnia są już same nieprawidłowe, to ew. może być.
                     for (; j < withoutFirst.Length; j++)
                     {
                         if (withoutFirst[j].Day != gs.Day) break;
                         if (withoutFirst[j].IsValid)
                         {
                             quiteOk = false;
                             break;
                         }
                     }
                     if (!quiteOk)
                     {*/
                    gap += DUPLICATE_SUBJECT_PENALTY;
                    if (doShit)
                        _localShitBuilder.AppendFormat("doubled subject: {0} (+{1})", gs, DUPLICATE_SUBJECT_PENALTY).AppendLine();
                    // }
                }
                usedSubjects[gs.Subject] = true;
                if (localGap == 0 && prev.IsValid)
                {
                    gap += SCHIZOFRENIA_PENALTY;
                    if (doShit) _localShitBuilder.AppendFormat("schizofrenia: {0} -> {1} (+{2})", gs, prev, SCHIZOFRENIA_PENALTY).AppendLine();
                }
                else if (localGap > 1 && gs.Day == prev.Day)
                {
                    gap += localGap + GAP_PENALTY;
                    if (doShit) _localShitBuilder.AppendFormat("gap: {0} -> {1} (+{2})", gs, prev, GAP_PENALTY).AppendLine();
                }
                /*if (prev.Classroom != gs.Classroom && gs.Day == prev.Day)
                {
                    gap += MOVE_ASS_PENALTY;
                    _shitBuilder.AppendFormat("move ass: {0} -> {1} (+{2})", gs, prev, MOVE_ASS_PENALTY).AppendLine();
                }*/

                if (gs.Day != prev.Day)
                {
                    int diff = Math.Abs(lessonsInDay - optimalLessonCount);
                    if (diff > 1)
                    {
                        gap += diff;
                        if (doShit) _localShitBuilder.AppendFormat("not optimal schooltime: {0} (+{1})", gs, diff).AppendLine();
                    }
                    lessonsInDay = 0;
                }
                else
                    lessonsInDay++;


                if (!_instance.Classrooms[gs.Classroom].IsCorrectSubject(gs.Subject))
                {
                    if (_instance.Subjects[gs.Subject].NeedsDedicatedClassroom)
                    {
                        gap += INCORRECT_ROOM_PENALTY;
                        if (doShit) _localShitBuilder.AppendFormat("bad room: {0} (+{1})", gs, INCORRECT_ROOM_PENALTY).AppendLine();
                    }
                    else
                    {
                        gap += INCORRECT_SUBJECT_PENALTY;

                        if (doShit) _localShitBuilder.AppendFormat("bad subject: {0} (+{1})", gs, INCORRECT_SUBJECT_PENALTY).AppendLine();
                    }
                }

                prev = gs;
            }
            if (doShit)
                lock (_shitBuilder)
                    _shitBuilder.Append(_localShitBuilder.ToString());
            return gap;
        }
        // TODO: uwzględnić, że grupy mogą mieć różne liczby
        // TODO: uwzgl., że są nauczyciele
        // TODO: musi być metoda losująca
        //      a) wartość indeksu (numer pozycji w perm.) mającą sens
        //      b) wartość permutacji mającą sens
        /*
         * mnożniki:
         * 
         */
        public int PlanShitness(bool doShits = false)
        {
            int shitness = 0;
            lock (_shitBuilder)
            {
                _shitBuilder.Clear();
                var plan = GetPlanInternal();


                //var groups = plan.GroupBy(gs => gs.Group).Select(gr => gr.Select(g => g).SortByTime());
                var groups = plan.GroupBy(gs => gs.Group).Select(gr => gr.Select(g => g));
                List<List<GroupSubject>> executed = new List<List<GroupSubject>>();
                foreach (var gs in groups)
                {
                    var list = gs.ToList();
                    list.Sort((a, b) => a.Time - b.Time);
                    executed.Add(list);
                }
                // Liczenie dziur w planie oraz nakładających się zajęć oraz
                //  ile razy dana grupa musi przejść z jednej sali do innej.
                


                for (int i = 0; i < executed.Count; i++)
                {
                    var group = executed[i];

                    shitness += GetGapsAndWalks(group, doShits);

                }/*
                var result = Parallel.For(0, executed.Count, (i, state) =>
                {
                    var group = executed[i];
                    shitness += GetGapsAndWalks(group, doShits);
                });
            //*/
                //shitness += groups.AsParallel().Sum(gss => GetGapsAndWalks(gss));

                Shits = _shitBuilder.ToString();
            }
            return shitness;
        }

        public Permutation Perm
        {
            get { return _perm; }
            set { _perm = value; }
        }

        public ScheduleSolution Clone()
        {
            return new ScheduleSolution(_instance, _perm.Clone());
        }
        /*
         * solution
         *
         */

        public override string ToString()
        {
            var b = new StringBuilder();
            int inroom = 0;
            int room = 0;

            Permutation inverse = _perm.Clone();
            inverse.Inverse();
            var plan = GetPlan().ToArray();
            for (int i = 0; i < plan.Length; i++)
            {
                GroupSubject gs = plan[i];
                b.AppendLine(gs.ToString());
            }
            return b.ToString();
        }
    }

}